package np13.sddb;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import np13.Slime;
import np13.Smell;
import np13.Snowden;
import np13.sddb.queries.GetRecordQuery;
import np13.sddb.queries.Query;

/**
 * The SDDB (Slime Destination Data Base) is used to administer the records of
 * all slimes. These records store to which homelands a slime has been sent to.
 * The SDDB stores and delivers records on demand. A client can request
 * something (modifying or delivering a record) by submitting a query which then
 * needs to be processed by the database. If a query yields a result (a record),
 * it has to be removed before a new result can be shown. Note that the database
 * is very inefficient since there can be only one result ready to be taken at
 * any one time. The database can, however, store arbitrary many queries at a
 * time.
 */
public class SDDB {

	/**
	 * The actual database.
	 */
	private final Map<Slime, List<Smell>> db = new HashMap<Slime, List<Smell>>();

	/**
	 * The display where the latest finished result is shown.
	 */
	private Result display;

	/**
	 * The queries that need to be processed.
	 */
	private final LinkedList<Query> inbox = new LinkedList<Query>();

	/**
	 * True, if the SDDB is started, false otherwise.
	 */
	private boolean started = false;

	/**
	 * A reentrant Lock for accessing inbox.
	 */
	private final Lock inboxLock = new ReentrantLock();
	/**
	 * A condition of the inbox lock.
	 */
	private final Condition inboxEmpty = inboxLock.newCondition();
	/**
	 * A reentrant Lock for accessing display.
	 */
	private final Lock displayLock = new ReentrantLock();
	/**
	 * A condition of the display lock.
	 */
	private final Condition result = displayLock.newCondition();

	/**
	 * Repeatedly waits for a query and processes it.
	 * 
	 * @throws InterruptedException
	 */
	private void eventLoop() throws InterruptedException {
		Query query = null;
		while (true) {
			// TODO letzte ueberpruefung
			// take the inbox lock
			inboxLock.lock();
			try {
				while (inbox.isEmpty() && started) {
					inboxEmpty.await();
				}
				// check if terminating signal has arrived
				if (!started) {
					System.out.println("SDDB ENDING");
					break;
				}
				// get a query for the inbox
				query = inbox.pop();
			} finally {
				// release the inbox lock
				inboxLock.unlock();
			}
			assert (query != null);
			Result res = query.applyToDB(db);
			// check if it was a GetResultQuery
			if (res == null) {
				if (query instanceof GetRecordQuery) {
					System.out.println("Got null from a GetRecordQuery");
					break;
				}
				// finished applying an AddSmellQuery
				continue;
			}
			// take the display lock
			displayLock.lock();
			try {
				while (display != null) {
					result.await();
				}
				// ueberpruefe IF
				if (res != null) {
					// put the result on a display and nofity all waiting
					display = res;
					result.signalAll();
				}
			} finally {
				// release the display lock
				displayLock.unlock();
			}
		}
	}

	/**
	 * Waits for a result to be displayed and returns it. Note that this method
	 * blocks until there is a finished record on the display for the requested
	 * slime. The result will be automatically removed.
	 * 
	 * @param slime
	 *            The slime whose record is of interest.
	 * @return The record of the slime.
	 * @throws InterruptedException
	 */
	public Result getResultForSlime(Slime slime) throws InterruptedException {
		displayLock.lock();
		try {
			while (display == null || !display.getSlime().equals(slime)) {
				result.await();
			}
			if (display.getSlime().equals(slime)) {
				Result res = display;
				pressAnyKey();
				return res;
			} else {
				// this code should not be reached. It is a insurance for the
				// compiler that the method always returns a result.
				System.out.println("-----------------");
				return null;
			}
		} finally {
			displayLock.unlock();
		}
	}

	/**
	 * Removes a result from the display.
	 * 
	 * @throws InterruptedException
	 */
	private void pressAnyKey() throws InterruptedException {
		display = null;
		result.signalAll();
		// man koennte auch signalAll in getResultForSlime Methode aufrufen in
		// finally Block, dann wuerden die Thread minimal kurzer auf Lock warten
	}

	/**
	 * Adds a query to the inbox queue. The query will be processed eventually,
	 * and the result, if applicable, will be placed on the display. The queries
	 * will be processed in a first-in-first-out-manner.
	 * 
	 * @param query
	 *            the query to be processed
	 */
	public void putQuery(Query query) {
		inboxLock.lock();
		try {
			assert (query != null);
			inbox.add(query);
			inboxEmpty.signalAll();
		} finally {
			inboxLock.unlock();
		}
	}

	/**
	 * Indicates that the database should start processing queries. May be
	 * called only once.
	 */
	public void start() {
		if (started)
			throw new IllegalStateException();
		started = true;
		// create a thread that runs the evenLoop, name it and start it.
		Thread thread = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					eventLoop();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
		System.out.println(thread.getName() + " is a SDDB!!");
		thread.setName(" SDDB ");
		thread.start();
	}

	/**
	 * This method is called by {@link Snowden} to terminate the {@link SDDB}
	 * once all Monads have terminated and he is putting his work to rest. As
	 * all Monads have terminated, there are no queries to be applied anymore.
	 */
	public void terminate() {
		inboxLock.lock();
		try {
			started = false;
			inboxEmpty.signalAll();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			inboxLock.unlock();
		}
	}
}
